/**
 * Copyright 2018 jianggujin (www.jianggujin.com).
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.jianggujin.modulelink.mvc.render;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.jianggujin.modulelink.mvc.JActionContext;
import com.jianggujin.modulelink.mvc.render.JErrorRender.JErrorRenderParam;
import com.jianggujin.modulelink.mvc.util.JIOUtils;
import com.jianggujin.modulelink.util.JAssert;
import com.jianggujin.modulelink.util.JStringUtils;

/**
 * 文件渲染
 * 
 * @author jianggujin
 *
 */
public class JFileRender implements JRender<File> {

    @Override
    public void render(JActionContext context, File param) throws IOException, ServletException {
        JAssert.checkNotNull(context, "context must not be null");
        JAssert.checkNotNull(param, "param must not be null");
        if (param == null || !param.isFile()) {
            JRenderManager.getInstance().getErrorRender().render(context, new JErrorRenderParam(404));
            return;
        }
        HttpServletRequest request = context.getRequest();
        HttpServletResponse response = context.getResponse();
        response.setHeader("Accept-Ranges", "bytes");
        response.setHeader("Content-disposition", "attachment; " + encodeFileName(request, param.getName()));
        response.setContentType("application/octet-stream");

        // ---------
        String rangeHeader = request.getHeader("Range");
        if (JStringUtils.isBlank(rangeHeader)) {
            response.setHeader("Content-Length", String.valueOf(param.length()));
            InputStream inputStream = null;
            OutputStream outputStream = null;
            try {
                inputStream = new BufferedInputStream(new FileInputStream(param));
                outputStream = response.getOutputStream();
                JIOUtils.copy(inputStream, outputStream);
            } catch (IOException e) { // ClientAbortException、EofException 直接或间接继承自
                                      // IOException
                String name = e.getClass().getSimpleName();
                if (name.equals("ClientAbortException") || name.equals("EOFException")) {
                } else {
                    throw new JRenderException(e);
                }
            } catch (Exception e) {
                throw new JRenderException(e);
            }
        } else {
            Long[] range = { null, null };
            processRange(range, rangeHeader, param);

            String contentLength = String.valueOf(range[1].longValue() - range[0].longValue() + 1);
            response.setHeader("Content-Length", contentLength);
            response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // status =
                                                                        // 206

            // Content-Range: bytes 0-499/10000
            StringBuilder contentRange = new StringBuilder("bytes ").append(String.valueOf(range[0])).append("-")
                    .append(String.valueOf(range[1])).append("/").append(String.valueOf(param.length()));
            response.setHeader("Content-Range", contentRange.toString());

            InputStream inputStream = null;
            OutputStream outputStream = null;
            try {
                long start = range[0];
                long end = range[1];
                inputStream = new BufferedInputStream(new FileInputStream(param));
                if (inputStream.skip(start) != start) {
                    throw new RuntimeException("File skip error");
                }
                outputStream = response.getOutputStream();
                byte[] buffer = new byte[1024];
                long position = start;
                for (int len; position <= end && (len = inputStream.read(buffer)) != -1;) {
                    if (position + len <= end) {
                        outputStream.write(buffer, 0, len);
                        position += len;
                    } else {
                        for (int i = 0; i < len && position <= end; i++) {
                            outputStream.write(buffer[i]);
                            position++;
                        }
                    }
                }
                outputStream.close();
                JIOUtils.safeClose(outputStream);
            } catch (IOException e) { // ClientAbortException、EofException 直接或间接继承自
                                      // IOException
                String name = e.getClass().getSimpleName();
                if (name.equals("ClientAbortException") || name.equals("EofException")) {
                } else {
                    throw new JRenderException(e);
                }
            } catch (Exception e) {
                throw new JRenderException(e);
            } finally {
                JIOUtils.safeClose(inputStream);
            }
        }
    }

    /**
     * 依据浏览器判断编码规则
     */
    public String encodeFileName(HttpServletRequest request, String fileName) {
        String userAgent = request.getHeader("User-Agent");
        try {
            String encodedFileName = URLEncoder.encode(fileName, "UTF8");
            // 如果没有UA，则默认使用IE的方式进行编码
            if (userAgent == null) {
                return "filename=\"" + encodedFileName + "\"";
            }

            userAgent = userAgent.toLowerCase();
            // IE浏览器，只能采用URLEncoder编码
            if (userAgent.indexOf("msie") != -1) {
                return "filename=\"" + encodedFileName + "\"";
            }

            // Opera浏览器只能采用filename*
            if (userAgent.indexOf("opera") != -1) {
                return "filename*=UTF-8''" + encodedFileName;
            }

            // Safari浏览器，只能采用ISO编码的中文输出,Chrome浏览器，只能采用MimeUtility编码或ISO编码的中文输出
            if (userAgent.indexOf("safari") != -1 || userAgent.indexOf("applewebkit") != -1
                    || userAgent.indexOf("chrome") != -1) {
                return "filename=\"" + new String(fileName.getBytes("UTF-8"), "ISO8859-1") + "\"";
            }

            // FireFox浏览器，可以使用MimeUtility或filename*或ISO编码的中文输出
            if (userAgent.indexOf("mozilla") != -1) {
                return "filename*=UTF-8''" + encodedFileName;
            }

            return "filename=\"" + encodedFileName + "\"";
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    protected void processRange(Long[] range, String rangeHeader, File param) {
        int index = rangeHeader.indexOf(',');
        if (index != -1) {
            rangeHeader = rangeHeader.substring(0, index);
        }
        rangeHeader = rangeHeader.replace("bytes=", "");

        String[] arr = rangeHeader.split("-", 2);
        if (arr.length < 2)
            throw new RuntimeException("Range error");

        long fileLength = param.length();
        for (int i = 0; i < range.length; i++) {
            if (!JStringUtils.isBlank(arr[i])) {
                range[i] = Long.parseLong(arr[i].trim());
                if (range[i] >= fileLength)
                    range[i] = fileLength - 1;
            }
        }

        // Range format like: 9500-
        if (range[0] != null && range[1] == null) {
            range[1] = fileLength - 1;
        }
        // Range format like: -500
        else if (range[0] == null && range[1] != null) {
            range[0] = fileLength - range[1];
            range[1] = fileLength - 1;
        }

        // check final range
        if (range[0] == null || range[1] == null || range[0].longValue() > range[1].longValue())
            throw new RuntimeException("Range error");
    }
}
